package com.jse;

import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import com.jse.json.Json;

/**
 * 实现了一个带有有效期的缓存。
 * 
 * @param <K> 键的类型String
 * @param <V> 值的类型Object
 */
public class Cache extends LinkedHashMap<String, Object> {

    private static final long serialVersionUID = Long.MAX_VALUE / 10;

    private final int maxSize;
    /**
     * 过期时间设置，默认为最大时间，可以通过构造函数修改。
     */
    private long EXPIRY;
    private HashMap<String, Long> ttlMap = new HashMap<>();
    private CacheListener listener;

    /**
     * 构造函数，使用默认的最大容量。
     */
    public Cache() {
        this(1024);
    }

    /**
     * 构造函数，指定最大容量。
     * 
     * @param maxSize 最大容量
     */
    public Cache(int maxSize) {
        this(maxSize, serialVersionUID);
    }

    /**
     * 构造函数，指定最大容量和过期时间。
     * 
     * @param maxSize 最大容量
     * @param ms 过期时间（毫秒）
     */
    public Cache(int maxSize, long ms) {
        this(maxSize, ms, 16, 0.75f, false);
    }

    /**
     * 构造函数，指定最大容量、过期时间、初始容量、加载因子和访问顺序。
     * 
     * @param maxSize 最大容量
     * @param ms 过期时间（毫秒）
     * @param initialCapacity 初始容量
     * @param loadFactor 加载因子
     * @param accessOrder 是否根据访问顺序排序
     */
    public Cache(int maxSize, long ms, int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor, accessOrder);
        this.maxSize = maxSize;
        this.EXPIRY = ms;
    }

    /**
     * 确定是否移除最老的条目以保持不超过最大容量。
     * 
     * @param eldest 最老的条目
     * @return 如果超过最大容量则返回 true，表示应该移除
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
        return this.size() > this.maxSize;
    }

    /**
     * 放入一个新的键值对，没有过期时间。
     * 
     * @param key 键
     * @param value 值
     * @return 旧的值，如果不存在则返回 null
     */
    public Object put(String key, Object value) {
        return put(key, value, 0);
    }

    /**
     * 放入一个新的键值对，并设置过期时间。
     * 
     * @param key 键
     * @param value 值
     * @param expiryTime 过期时间（毫秒）
     * @return 旧的值，如果不存在则返回 null
     */
    public Object put(String key, Object value, long expiryTime) {
        if (expiryTime > 0) {
            ttlMap.put(key, System.currentTimeMillis() + expiryTime);
        }
        if (listener != null) {
            if (value instanceof Map || value instanceof Collection) {
                listener.put(Jse.redisPrefix + ":" + key, Json.toJson(value), expiryTime);
            } else {
                listener.put(Jse.redisPrefix + ":" + key, value.toString(), expiryTime);
            }
        }
        return super.put(key, value);
    }

    /**
     * 检查键是否存在并且未过期。
     * 
     * @param key 键
     * @return 如果键存在并且未过期则返回 true
     */
    public boolean containsKey(Object key) {
        return !checkExpiry(key, true) && super.containsKey(key);
    }

    /**
     * 获取一个键对应的值，如果过期则返回 null。
     * 
     * @param key 键
     * @return 对应的值或者 null
     */
    public Object get(Object key) {
        if (key == null) {
            return null;
        }
        if (checkExpiry(key, true)) { // 已过期
            return null;
        }
        Object v = super.get(key);
        if (v == null && listener != null) {
            Object x = listener.get(Jse.redisPrefix + ":" + key);
            String s = (String) x;
            if (s.charAt(0) == '[' || s.charAt(0) == '{') {
                return Json.parse(s);
            }
            return v;
        }
        return v;
    }

    /**
     * 获取一个键对应的值，如果值为 null 则返回默认值。
     * 
     * @param key 键
     * @param d 默认值
     * @return 对应的值或者默认值
     */
    public <T> T get(Object key, T d) {
        Object v = get(key);
        return v == null ? d : (T) v;
    }

    /**
     * 检查一个键是否过期。
     * 
     * @param key 键
     * @return 如果键过期则返回 -1，否则返回对应的值
     */
    public Object isInvalid(Object key) {
        if (key == null)
            return null;
        if (!ttlMap.containsKey(key)) {
            return null;
        }
        long expiryTime = ttlMap.get(key);
        boolean flag = System.currentTimeMillis() > expiryTime;
        if (flag) {
            super.remove(key);
            ttlMap.remove(key);
            return -1;
        }
        return super.get(key);
    }

    /**
     * 检查一个键是否过期。
     * 
     * @param key 键
     * @param isRemoveSuper 是否从 LinkedHashMap 中移除过期项
     * @return 如果键过期则返回 true，否则返回 false
     */
    private boolean checkExpiry(Object key, boolean isRemoveSuper) {
        if (!ttlMap.containsKey(key)) {
            return Boolean.FALSE;
        }
        long expiryTime = ttlMap.get(key);
        boolean flag = System.currentTimeMillis() > expiryTime;
        if (flag) {
            if (isRemoveSuper)
                super.remove(key);
            ttlMap.remove(key);
        }
        return flag;
    }

    /**
     * 设置监听器。
     * 
     * @param listener 监听器
     * @return 当前缓存实例
     */
    public Cache setListener(CacheListener listener) {
        this.listener = listener;
        return this;
    }

    /**
     * 移除一个键值对。
     * 
     * @param key 键
     * @return 被移除的值，如果不存在则返回 null
     */
    @Override
    public Object remove(Object key) {
        if (listener != null)
            listener.remove(Jse.redisPrefix + ":" + key);
        ttlMap.remove(key);
        return super.remove(key);
    }

    /**
     * 缓存监听器接口。
     */
    public interface CacheListener {
        /**
         * 获取一个键对应的值。
         * 
         * @param k 键
         * @return 对应的值
         */
        Object get(Object k);

        /**
         * 放入一个新的键值对。
         * 
         * @param k 键
         * @param v 值
         * @param ttl 过期时间（毫秒）
         */
        void put(String k, Object v, long ttl);

        /**
         * 移除一个键值对。
         * 
         * @param k 键
         */
        void remove(Object k);

        /**
         * 初始化监听器。
         */
        void init();
    }

    /**
     * 主方法用于测试。
     * 
     * @param args 命令行参数
     * @throws InterruptedException 如果线程被中断
     */
    public static void main(String[] args) throws InterruptedException {
        Cache map = new Cache(2);
        map.put("test", "ankang1");
        map.put("test1", "ankang2");
        map.put("test2", "ankang3", 100);
        Thread.sleep(1000);
        for (String k : map.keySet()) {
            System.out.println(k + ":" + map.get(k));
        }
        System.out.println(Json.parse("[1]"));
    }
}